package com.xiaojie.export;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.xiaojie.conf.ImportExportConf;
import com.xiaojie.context.DataInfoContext;
import com.xiaojie.dao.EDIDataOperate;
import com.xiaojie.dao.Param;
import com.xiaojie.error.ExportException;
import com.xiaojie.ext.AfterExportData;
import com.xiaojie.ext.BeforeExportData;
import com.xiaojie.ext.ExtensionCallRelSystem;
import com.xiaojie.ext.cust.CustExportExecute;
import com.xiaojie.parse.DataModelParse;
import com.xiaojie.parse.model.DataModel;
import com.xiaojie.parse.model.RefTable;
import com.xiaojie.parse.model.Settings;
import com.xiaojie.parse.model.Table;
import com.xiaojie.progress.ExportImportProgress;
import com.xiaojie.progress.ExportStage;
import com.xiaojie.toolkits.FieldNameUtil;
import com.xiaojie.toolkits.ZipUtils;
import lombok.extern.slf4j.Slf4j;
import net.lingala.zip4j.ZipFile;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author wangye
 * @classname ExportData
 * @date 2021/6/25 14:56
 **/
@Component
@Slf4j
public class ExportData {

    @Autowired
    private DataModelParse parse;
    @Autowired
    private EDIDataOperate EDIDataOperate;

    @Autowired(required = false)
    private AfterExportData afterExportData;

    @Autowired(required = false)
    private BeforeExportData beforeExportData;

    @Autowired(required = false)
    private ExtensionCallRelSystem extensionCallRelSystem;

    @Autowired
    private ImportExportConf conf;


    public byte[] exportZipByte(String configFilePath, Map paramMap) throws Exception {
        return exportZipByte(configFilePath, paramMap, false, null, null, null, null);
    }

    public byte[] exportZipByte(String configFilePath, Map paramMap, CustExportExecute exportExecute) throws Exception {
        return exportZipByte(configFilePath, paramMap, false, null, null, exportExecute, null);
    }

    public ZipFile exportZipFile(String configFilePath, Map paramMap, boolean isSecret,
                                 String password, String workPath, String zipName,
                                 CustExportExecute exportExecute,
                                 Function<String, String> custGetFileContent) throws Exception {
        return exportZipFile(configFilePath, paramMap, isSecret, password, workPath, zipName, null, exportExecute, custGetFileContent);
    }

    /**
     * 导出zip文件
     *
     * @param configFilePath
     * @param paramMap
     * @param isSecret
     * @param password
     * @param workPath
     * @param zipName
     * @param exportProgress
     * @param exportExecute  扩展逻辑
     * @return
     * @throws IOException
     */
    public ZipFile exportZipFile(String configFilePath, Map paramMap, boolean isSecret,
                                 String password, String workPath, String zipName,
                                 ExportImportProgress exportProgress,
                                 CustExportExecute exportExecute,
                                 Function<String, String> custGetFileContent) throws Exception {
        if (exportProgress != null) {
            DataInfoContext.get().setProgress(exportProgress);
            exportProgress.start();
        }
        //获取配置文件内容
        DataModel dataModel = parse.getDataModel(custGetFileContent, configFilePath, Boolean.TRUE);

        String prefixfileName = String.valueOf(System.currentTimeMillis());

        //工作目录
        if (workPath == null) {
            workPath = FileUtil.getTmpDirPath();
        }
        //临时目录
        String tempDirPath = workPath + prefixfileName + File.separator;
        File tempDir = new File(tempDirPath);
        File file = null;
        ZipFile zipFile = null;
        try {
            if (!tempDir.exists()) {
                tempDir.mkdirs();
            }
            log.debug("export_tempDirPath:" + tempDirPath);
            //1.导出本系统数据
            Map<String, List<Map>> selfData = exportData(dataModel, paramMap, tempDir, exportExecute);
            String json = JSON.toJSONString(selfData);
            file = new File(tempDirPath +
                    (StringUtils.isNotBlank(dataModel.getFileName()) ? dataModel.getFileName() : conf.DEFAULT_DB_FILE_NAME));
            FileUtil.writeString(json, file, "UTF-8");

            //2.导出其他系统数据
            Map<String, com.xiaojie.parse.model.RelOtherSystem> relOtherSystems = dataModel.getRelOtherSystem();
            if (relOtherSystems != null) {
                exportRelOtherSystem(tempDirPath, selfData, relOtherSystems);
            }
            //3.压缩整个临时目录文件到工作目录（和临时目录同级，最后删除临时目录）
            zipFile = doZipWorkDir(isSecret, password, workPath, zipName, dataModel, prefixfileName, tempDir);
        } catch (Exception e) {
            log.error("导出失败");
            log.error(e.getMessage(), e);
            throw e;
        } finally {
            if (tempDir.exists()) {
                FileUtil.del(tempDirPath);
            }
        }
        return zipFile;
    }

    public ZipFile doZipWorkDir(boolean isSecret, String password, String workPath, String zipName, DataModel dataModel, String prefixfileName, File tempDir) {
        ExportStage.ZIP_DIR.progress(0);
        ZipFile zipFile = ZipUtils.zipFileAndEncrypt(workPath + (zipName == null ? dataModel.getSystemName() + "_" + prefixfileName : zipName),
                tempDir, password, "UTF-8", isSecret);
        ExportStage.ZIP_DIR.progress(100);
        return zipFile;
    }

    /**
     * 导出数据文件压缩包并加密
     *
     * @param configFilePath 配置文件地址
     * @param paramMap       查询数据参数
     * @param isSecret       是否加密
     * @param password
     * @auther wangye
     * @date 2021/6/30
     */
    public byte[] exportZipByte(String configFilePath, Map paramMap, boolean isSecret, String password, String workPath,
                                CustExportExecute exportExecute, Function<String, String> custGetFileContent) throws IOException {
        byte[] byteData = null;
        ZipFile zipFile = null;
        try {
            zipFile = exportZipFile(configFilePath, paramMap, isSecret, password, workPath, null, exportExecute, custGetFileContent);
            byteData = FileUtil.readBytes(zipFile.getFile());
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException(e);
        } finally {
            if (zipFile != null && zipFile.getFile() != null && zipFile.getFile().exists()) {
                zipFile.getFile().delete();
            }
        }
        return byteData;
    }


    public void exportRelOtherSystem(String tempDirPath, Map<String, List<Map>> selfData, Map<String, com.xiaojie.parse.model.RelOtherSystem> relOtherSystems) {
        int size = relOtherSystems.size();
        for (String systemName : relOtherSystems.keySet()) {
            ExportStage.EXPORT_REL_SYSTEM.progress(0, systemName);

            com.xiaojie.parse.model.RelOtherSystem relOtherSystem = relOtherSystems.get(systemName);
            //1 拼接其他系统导出接口
            String apiUrl = relOtherSystem.getExportUrl(systemName);
            //拼接参数
            boolean httpPramsHaveData = false;
            Map<String, Object> httpPrams = new HashMap<>();
            for (String paramsName : relOtherSystem.getParams().keySet()) {
                List<RefTable> refTables = relOtherSystem.getParams().get(paramsName);
                List<Object> filedData = new ArrayList<>();
                refTables.forEach(o -> {

                    List<Map> rows = selfData.get(o.getTableName());
                    filedData.addAll(Optional.ofNullable(rows).orElse(new ArrayList<>()).stream()
                            .filter(m -> m.get(o.getRefField()) != null)
                            .map(m -> m.get(o.getRefField())).collect(Collectors.toList()));
                });
                if (filedData.size() != 0) {
                    httpPrams.put(paramsName, filedData);
                }
            }

            //扩展点
            if (extensionCallRelSystem != null) {
                extensionCallRelSystem.beforeProcess(systemName, httpPrams, selfData);
            }

            httpPramsHaveData = MapUtil.isNotEmpty(httpPrams);
            if (!httpPramsHaveData) {
                //没有一个参数，则跳过该阶段
                log.error("导出跳过:关联系统" + systemName + " 没有所需参数");
                continue;
            }
            HttpResponse response = null;
            try {
                log.error(systemName + apiUrl + "relParams:" + JSON.toJSONString(httpPrams));
                response = HttpRequest.post(apiUrl)
                        .form("json", JSON.toJSONString(httpPrams))
                        .timeout(5 * 60000)
                        .execute();
                if (extensionCallRelSystem != null)
                    extensionCallRelSystem.afterProcess(systemName, response);
                //根据配置文件里的关联系统名称进行zip保存
                checkExportRelSysResult(systemName, response);
                FileUtil.writeFromStream(response.bodyStream(), tempDirPath + systemName + ".zip");
            } catch (Exception e) {
                log.error(systemName + "导出失败:" + (response == null ? "没有响应信息" : response.body()));
                log.error(e.getMessage(), e);
                throw new ExportException("导出失败:关联系统" + systemName + "导出失败");
            }

            ExportStage.EXPORT_REL_SYSTEM.progress((int) (1f / size * 100), systemName);
        }
    }

    /**
     * 校验关联系统导出结果
     *
     * @param systemName
     * @param response
     */
    private void checkExportRelSysResult(String systemName, HttpResponse response) {
        String header = response.header("Content-Disposition");
        if (header != null && !"".equals(header)) {
            if (header.contains("filename")) {
                return;
            }
        }
        throw new ExportException(systemName + "导出返回不是文件，" + response.body());
    }

    /**
     * 导出二级制数据方便下载
     *
     * @param configFilePath
     * @param paramMap
     * @auther wangye
     * @date 2021/6/29
     */
    public byte[] exportByte(String configFilePath, Map paramMap, CustExportExecute exportExecute) throws Exception {
        return ZipUtils.gzip(exportStr(configFilePath, paramMap, exportExecute), "UTF-8");
    }

    /**
     * 导出json格式字符串数据
     *
     * @param configFilePath
     * @param paramMap
     * @param tempDir        工作目录
     * @auther wangye
     * @date 2021/6/29
     */
    public Map<String, List<Map>> exportData(String configFilePath, Map paramMap, File tempDir,
                                             CustExportExecute exportExecute, Function<String, String> custGetFileContent) throws Exception {
        //获取配置文件内容
        DataModel dataModel = parse.getDataModel(custGetFileContent, configFilePath, Boolean.TRUE);
        return exportData(dataModel, paramMap, tempDir, exportExecute);
    }

    public Map<String, List<Map>> exportData(DataModel dataModel, Map paramMap, File tempDir, CustExportExecute exportExecute) throws IOException {

        if (dataModel == null) {
            throw new RuntimeException("配置文件不存在或文件内容为空");
        }

        DataInfoContext.get().setDataModelInfo(dataModel);
        //已查询出来的表数据
        Map<String, List<Map>> tableData = Maps.newHashMap();
        try {
            //导出前扩展点
            exportExecute = exportExecute == null ? new CustExportExecute() {
            } : exportExecute;
            exportExecute.beforeExportData().execute(dataModel, tableData);

            //全局配置
            Settings globalSettings = dataModel.getSettings();
            //表配置
            LinkedHashMap<String, Table> tables = dataModel.getTables();
            //查询数据，查询条件优先级，1.表中自定义的查询规则进行查询，2.根据依赖表查询，3.根据全局查询规则进行查询

            // 导出数据
            doExportData(paramMap, globalSettings, tables, tableData);

            //导出后扩展点
            exportExecute.afterExportData().execute(dataModel, tableData);

            //获取数据不完整打印日志文件
            if (tableData.size() < tables.size()) {
                List<String> tableNames = tableData.entrySet().stream().map(Map.Entry::getKey).collect(Collectors.toList());
                List<String> tableNames2 = tables.entrySet().stream().map(Map.Entry::getKey).collect(Collectors.toList());
                tableNames2.removeAll(tableNames);
                log.error(String.format("%s没有获取到数据", CollectionUtil.join(tableNames2, ",")));
            }
        } finally {
            DataInfoContext.remove();
        }


        return tableData;
    }

    private void doExportData(Map paramMap, Settings globalSettings, LinkedHashMap<String, Table> tables, Map<String, List<Map>> tableData) {
        //已查询的表数量
        int queriedTableCount = 0;
        do {
            //每次循环 设置 已查询的表数量
            queriedTableCount = tableData.size();
            //循环表配置
            for (Map.Entry<String, Table> entry : tables.entrySet()) {
                Table table = entry.getValue();
                String sqlSplit = table.getRefTablesJoint();
                //表名
                String tableName = entry.getKey();
                if (CollectionUtil.isNotEmpty(tableData.get(tableName)))
                    continue;

                //⬇拼接/查询 参数params 和 查询数据list
                List<Map> dataList = Lists.newArrayList();
                List<Param> params = null;

                //1.获取 表级 查询条件 Map<字段名,值>
                Map<String, String> queryParams = Optional.ofNullable(table.getSettings())
                        .map(settings -> settings.getQueryByParams()).orElse(null);

                //2.判断 -> 条件优先级，1.表中自定义的查询规则进行查询，2.根据依赖表查询，3.根据全局查询规则进行查询
                if (queryParams != null) {
                    //2.1 表级查询条件，表中自定义的查询规则进行查询
                    //拼接 字段名:值 -> params
                    params = paraMap2ParamList(queryParams, paramMap);

                } else if (CollectionUtil.isNotEmpty(table.getRefTables())) {

                    //没有参数就判断是否有关联表
                    //2.2 关联表，settings中没有配置查询条件则使用关联表中第一个作为条件进行查询
                    List<RefTable> refTables = table.getRefTables();

                    //关联表是否有数据
                    boolean refTableHaveData = false;
                    //关联表是否都查询过
                    boolean refTableAllSelected = true;

                    for (RefTable refTable : refTables) {
                        List<Map> refTableData = tableData.get(refTable.getTableName());
                        if (refTableData == null) {
                            refTableAllSelected = false;
                            break;
                        }
                        //有一个关联表有数据即认为 关联表有数据
                        if (refTableData.size() > 0) {
                            refTableHaveData = true;
                        }
                    }
                    if (refTableAllSelected) {
                        Map<String, Param> refParamMap = new HashMap<>();
                        if (refTableHaveData) {
                            for (RefTable refTable : refTables) {
                                String srcField = refTable.getSrcField();
                                Param param = refParamMap.get(srcField);
                                List<Object> objects = tableData.get(refTable.getTableName()).stream()
                                        .map(m -> m.get(FieldNameUtil.convert(refTable.getRefField())))
                                        .filter(value -> value != null)
                                        .collect(Collectors.toList());
                                //参数级别  有数据进行拼接，防止or sql in 空气
                                if (objects.size() > 0) {
                                    if (param == null) {
                                        param = Param.builder()
                                                .name(refTable.getSrcField())
                                                .value(objects)
                                                .build();
                                        refParamMap.put(srcField, param);
                                    } else {
                                        List<Object> value = (List<Object>) param.getValue();
                                        value.addAll(objects);
                                    }
                                }
                            }
                        }
                        params = new ArrayList<>(refParamMap.values());
                    } else {
                        //没有 ->  则跳过该表（Q:如果表顺序有问题会导致表数据无法查询出来 A:不会，会循环
                        continue;
                    }
                } else {
                    List<String> excludeGlobalSettingItems = Optional.ofNullable(table.getSettings())
                            .flatMap(settings -> Optional.ofNullable(settings.getExcludeGlobalSettingItems()))
                            .orElse(Lists.newArrayList());
                    if (!excludeGlobalSettingItems.contains("queryByParams")) {
                        //没有关联表 就 使用全局级 查询条件
                        //拼接 字段名:值 -> params
                        params = paraMap2ParamList(globalSettings.getQueryByParams(), paramMap);
                    }
                }
                //通过 params 查询对应表数据
                ExportStage.EXPORT_DATABASE.progress(0, tableName);
                log.debug(String.format("查询%s数据", tableName));
                if (CollectionUtil.isNotEmpty(params) || table.getSettings().isQueryAll()) {
                    dataList = EDIDataOperate.selectData(tableName, "*", params, sqlSplit);
                }
                tableData.put(tableName, dataList);
                ExportStage.EXPORT_DATABASE.progress((int) (1f / queriedTableCount * 100), tableName);
            }
            //循环判断条件
            //tableData.size() < tables.size() 次数
            // tableData.size() != queriedTableCount 是否有 新的数据 查询出来
            //跳出循环
        } while (tableData.size() < tables.size() && tableData.size() != queriedTableCount);
    }

    public String exportStr(String configFilePath, Map paramMap, CustExportExecute exportExecute) throws Exception {
        return JSON.toJSONString(exportData(configFilePath, paramMap, null, exportExecute,null));
    }


    private List<Param> paraMap2ParamList(Map<String, String> paramFields, Map paramValue) {
        List<Param> params = Optional.ofNullable(paramFields)
                .map(map -> map.entrySet().stream().map(e -> Param.builder()
                        .name(e.getKey()).value(paramValue.get(e.getValue())).build()).collect(Collectors.toList()))
                .orElse(null);
        //新增处理空参数
        if (params.size() > 0) {
            Object value = params.get(0).getValue();
            if (value instanceof Collection) {
                if (((Collection) value).size() == 0) {
                    return null;
                }
            }
        }
        //else doNothing ,当size=0时表示查询全部数据
        return params;
    }

}
